<template>
  <div>
    <el-select
      ref="selectTreeRef"
      clearable
      @clear="clearSelect"
      placeholder="请选择"
      v-model="selectOption">
      <el-option :value="nodeId" :label="nodeName">
        <el-tree
          ref="treeRef"
          :node-key="treeProps.value"
          :data="treeData"
          :props="treeProps"
          @node-click="handleNodeClick"
          :expand-on-click-node="false"
          highlight-current
          default-expand-all/>
      </el-option>
    </el-select>
  </div>
</template>
<script lang="ts" setup>
/*
* @author ljy
* 20220401
* 特别说明：20220403,element-plus开发了TreeSelect选择树，应优先考虑此插件
* 自定义树形插件选择框，由el-select和el-tree两个组件构成。本质上是select中只有一个option选项，该option选项就包含了el-tree菜单，然后通过处理他们之间的关系保证插件的交互。
 一、用法
（一）基本用法（例子参照部门菜单：@/components/uem/DepartmentTreeSelect.vu）
1.1 引入组件的方式
import TreeSelect from '@/components/TreeSelect.vue';
1.2 使用方式
<TreeSelect
    v-model="departmentId"
    :treeProps="defaultProps"
    :treeData="departmentTree"
    @changeSelectTreeNode="changeDepartment"
  />
1.3 参数说明：
（1）v-model:双向绑定的参数，对应组件的modelValue属性。建议手动对该参数的computed和watch进行相应处理
（2）treeProps用于描述etree的配置选项，具体看el-tree的Props属性描述
备注：本组件增加了value属性
（3）treeData，el-tree的data属性，详见el-tree的data属性描述
（4）changeSelectTreeNode
当点击节点时触发，一般用于配合父组件执行，例如改变选中节点后，自动刷新执行表单查询
*/
import type { ElTree } from 'element-plus';
import {
  getCurrentInstance, ref, defineProps, computed, watch, WritableComputedRef, withDefaults,
} from 'vue';

const { proxy } = getCurrentInstance(); // 使用proxy代替ctx，因为ctx只在开发环境有效
// vue3中使用了modelValue来替代value
// 也就是说vue3中，value改成了modelValue，input方法了改成update:modelValue
interface Props {
  modelValue: any, // 获取从父组件input类型绑定的值,vue3中使用了modelValue来替代value，并且传递过来的是非ref或reactive类型的值,input方法了改成update:modelValue
  treeProps?:{ // 具体看el-tree的Props属性描述。本组件增加了value属性
    value:string,
    label:string,
    children:string
  },
  treeData:any, // 应该是一个数组
}
const props = withDefaults(defineProps<Props>(), {
  modelValue: () => null,
  treeProps: () => ({
    value: 'id',
    label: 'name',
    children: 'children',
  }),
  treeData: () => ref([]),
});
// 定义需要的属性
const selectTreeRef = ref();// 定义树形下拉框单的ref对象
const treeRef = ref<InstanceType<typeof ElTree>>();// 定义树形菜单的ref对象
const nodeId = ref('');// 定义tree的节点id
const nodeName = ref('');// 定义tree的节点名称

// 加载部门属性菜单
interface Tree { // 树形接口类型
  [index: string]:string|[];
}
// 与父组件v-model绑定的值进行关联
const selectOption = ref<WritableComputedRef<string|null>>(computed({ // 要转化为ref对象
  get() { // 当父组件的值改变时触发
    // 不能在get里面改变nodeName.value的值，所以在watch里面进行修改
    // nodeName.value=treeRef.value.getNode(props.modelValue).data["name"];
    return props.modelValue;
  },
  set(val) { // 当本组件的值改变时触发
    // 下面两个emit的顺序不能调换，根据组件之间逻辑决定
    // 则本组件选择的值（tree选中节点的键）传递给父组件
    proxy.$emit('update:modelValue', val);
    // 当点击节点时，说明选中值可能改变，此时提供给父组件调用。例如修改值后更新查询表单的结果
    proxy.$emit('changeSelectTreeNode', val);
  },
}));
// 根据下拉框的初始值，显示tree已选中节点，并显示已选中节点的值在下拉框默认值中
watch(selectOption, (newVal:string|null, oldVal:string|null):void => { // 主要是为了解决在组件外部改变下拉框值时，让下拉框显示的值进行相应变化。因为在compute的get属性更改报错，故放在这里解决
  if (newVal) { // 如果select有初始值，则让select正常显示选中的值和标签；如果是null表示已清空选择项，例如查询表单点击重置,则不执行操作，否则报错。
    nodeName.value = treeRef.value!.getNode(newVal).data[props.treeProps.label];// 下拉框option的label值=节点的标签值
    nodeId.value = treeRef.value!.getNode(newVal).data[props.treeProps.value];// 下拉框select的v-model绑定的value值=节点的键值
    treeRef.value!.setCurrentKey(newVal);// 设置本组件中的tree默认选中的节点与外部值一致
  } else { // 如果值为空，例如清空select的按钮
    treeRef.value!.setCurrentKey(null);// 清空已经选中的值。null会报错，通过查看源码知道tree的key属性为string|number，不过一样可以实现清空。
  }
});
// 当点击树形菜单的节点时，改变下拉框属性的值
const handleNodeClick = (data: Tree) => { // 获取点击数节点的信息，包含该节点的children
  nodeId.value = data[props.treeProps.value] as string; // 下拉框option的value值=节点的键值
  nodeName.value = data[props.treeProps.label] as string;// 下拉框option的label值=节点的标签值
  selectOption.value = data[props.treeProps.value] as string;// 下拉框select的v-model绑定的value值=节点的键值
  // 选择器执行完成后，使其失去焦点实现隐藏下拉框的效果。
  selectTreeRef.value.blur();
};
// 当对下拉框点击clear按钮
const clearSelect = () => {
  selectOption.value = null;// 清空下拉框的值
};
</script>
<style lang="scss" scoped>
.el-select-dropdown__item{  /*修改下拉菜单的样式*/
  height:auto;/*修改下拉框的高度为自动，否则只显示一个option的高度*/
  padding:0;/*因为option默认是有padding的，这样tree无法扩充到整个option的宽度*/
  font-weight: normal;/*在封装为组件后，font-wight会变为700数值，导致菜单字体加粗，这里特意设置恢复为原来的字体*/
}
</style>
